package pl.eurekin;

import com.sun.tools.javac.tree.JCTree.JCIdent;
import com.sun.tools.javac.tree.JCTree.JCMethodDecl;
import com.sun.tools.javac.code.Flags;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.code.TypeTags;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.JCTree.JCAnnotation;
import com.sun.tools.javac.tree.JCTree.JCBlock;
import com.sun.tools.javac.tree.JCTree.JCClassDecl;
import com.sun.tools.javac.tree.JCTree.JCExpression;
import com.sun.tools.javac.tree.JCTree.JCFieldAccess;
import com.sun.tools.javac.tree.JCTree.JCLiteral;
import com.sun.tools.javac.tree.JCTree.JCMethodInvocation;
import com.sun.tools.javac.tree.JCTree.JCNewClass;
import com.sun.tools.javac.tree.JCTree.JCStatement;
import com.sun.tools.javac.tree.JCTree.JCTypeParameter;
import com.sun.tools.javac.tree.JCTree.JCVariableDecl;
import com.sun.tools.javac.tree.TreeMaker;
import com.sun.tools.javac.util.List;
import com.sun.tools.javac.util.ListBuffer;
import com.sun.tools.javac.util.Name;

import lombok.AccessLevel;
import lombok.core.AnnotationValues;
import lombok.javac.JavacAnnotationHandler;
import lombok.javac.JavacNode;
import lombok.javac.handlers.HandleGetter;
import static lombok.javac.handlers.JavacHandlerUtil.*;


public class HandleBindableData implements JavacAnnotationHandler<BindableData> {

    @Override
    public boolean handle(AnnotationValues<BindableData> annotation,
            JCAnnotation ast, JavacNode annotationNode) {

        // we've got the annotation node - just get annotated object
        JavacNode typeNode = annotationNode.up();

        if (!ensureAnnotIsOnClass(annotationNode, typeNode)) {
            return true; // false means we want to be invoked again
        }

        String fieldName = "$propertyChangeSupport";
        TreeMaker maker = typeNode.getTreeMaker();
        if (fieldExists(fieldName, typeNode) == MemberExistsResult.NOT_EXISTS) {
            JCVariableDecl newVar = constructPropertyChangeSupportDeclField(maker, typeNode);
            injectField(typeNode, newVar);
        }


        JCMethodDecl newMethod1 = constructAddListenerMethod(maker, typeNode);
        JCMethodDecl newMethod2 = constructRemoveListenerMethod(maker, typeNode);
        injectMethod(typeNode, newMethod1);
        injectMethod(typeNode, newMethod2);

        new HandleGetter().generateGetterForType(typeNode, annotationNode, AccessLevel.PUBLIC, true);
        new HandlePropertySetter().generateSetterForType(typeNode, annotationNode, AccessLevel.PUBLIC, true);
//        new HandleToString().generateToStringForType(typeNode, annotationNode);
//        String staticConstructorName = annotation.getInstance().staticConstructor();
//        new HandleConstructor().generateRequiredArgsConstructor(typeNode, AccessLevel.PUBLIC, staticConstructorName, true);
//        new HandleEqualsAndHashCode().generateEqualsAndHashCodeForType(typeNode, annotationNode);


        return true;
    }

    private JCVariableDecl constructPropertyChangeSupportDeclField(TreeMaker maker, JavacNode typeNode) {
        JCExpression objectType = null;
        JCExpression constructedType = null;
        objectType = chainDots(maker, typeNode, "java", "beans", "PropertyChangeSupport");
//        constructedType = chainDots(maker, typeNode, "javax", "swing", "event", "SwingPropertyChangeSupport");
        //argument list for constructortree Maker.Ident(field.toName("this"))

        ListBuffer<JCExpression> myArgs = new ListBuffer<JCExpression>();
//                JCLiteral trueLiteral = maker.Literal(Boolean.TRUE);
        JCLiteral t2 = maker.Literal(TypeTags.BOOLEAN, 1);
        constructedType = chainDots(maker, typeNode, "java", "beans", "PropertyChangeSupport");
        JCIdent thisIdent = maker.Ident(typeNode.toName("this"));
        myArgs.append(thisIdent);
//        myArgs.append(t2);
        // Here is my guess:
        JCNewClass newClass = maker.NewClass(
                null,
                List.<JCExpression>nil(),
                constructedType,
                myArgs.toList(),
                null); // null - we don't want class body (anonynous or regular)
        JCVariableDecl newVar = maker.VarDef(
                maker.Modifiers(Flags.PRIVATE),
                typeNode.toName("$propertyChangeSupport"), objectType,
                null //newClass - WTF, why can't I haz normal init here?
                );
        return newVar;
    }

    private JCTree.JCMethodDecl constructAddListenerMethod(
            TreeMaker treeMaker, JavacNode field) {
        int access = Flags.PUBLIC;
        List<JCStatement> statements;

        JCFieldAccess pChSupp = treeMaker.Select(
                treeMaker.Ident(field.toName("$propertyChangeSupport")),
                field.toName("addPropertyChangeListener"));
        JCMethodInvocation callPChSupp = treeMaker.Apply(
                List.<JCExpression>nil(),
                pChSupp,
                List.<JCExpression>of(treeMaker.Ident(field.toName("listener"))));


        JCExpression fieldEqualsNull = treeMaker.Binary(JCTree.EQ, treeMaker.Ident(field.toName("$propertyChangeSupport")), treeMaker.Literal(TypeTags.BOT, null));
        JCExpression constructedType = chainDots(treeMaker, field, "java", "beans", "PropertyChangeSupport");
        JCIdent thisLiteral = treeMaker.Ident(field.toName("this"));
        ListBuffer<JCExpression> myArgs = new ListBuffer<JCExpression>();
        myArgs.append(thisLiteral);
        JCNewClass newPCh = treeMaker.NewClass(null, List.<JCExpression>nil(), constructedType, myArgs.toList(), null);
        JCStatement declareField = treeMaker.Exec(treeMaker.Assign(treeMaker.Ident(field.toName("$propertyChangeSupport")), newPCh));
        JCStatement unNull = treeMaker.If(fieldEqualsNull, declareField, null);


        statements = List.<JCStatement>of(unNull, treeMaker.Exec(callPChSupp));


        JCBlock methodBody = treeMaker.Block(0, statements);


        JCExpression methodVarType = chainDots(treeMaker, field, "java", "beans",
                "PropertyChangeListener");
        JCVariableDecl param =
                treeMaker.VarDef(
                treeMaker.Modifiers(Flags.FINAL, List.<JCAnnotation>nil()),
                field.toName("listener"),
                methodVarType, // java.beans.PropertyChangeListener
                null);


        List<JCVariableDecl> parameters = List.of(param);
        List<JCExpression> throwsClauses = List.nil(); //                 ok
        List<JCTypeParameter> methodGenericParams = List.nil(); //        ok
        JCExpression annotationMethodDefaultValue = null; //              ok
        Name methodName = field.toName("addPropertyChangeListener"); //    ok
        JCExpression methodType = treeMaker.Type(new Type(TypeTags.VOID, null)); //ok
        return treeMaker.MethodDef(
                treeMaker.Modifiers(access, List.<JCAnnotation>nil()),
                methodName,
                methodType,
                methodGenericParams,
                parameters,
                throwsClauses,
                methodBody,
                annotationMethodDefaultValue);
    }

    private JCTree.JCMethodDecl constructRemoveListenerMethod(
            TreeMaker treeMaker, JavacNode field) {
        int access = Flags.PUBLIC;
        List<JCStatement> statements;

        JCFieldAccess pChSupp = treeMaker.Select(treeMaker.Ident(field.toName("$propertyChangeSupport")), field.toName("removePropertyChangeListener"));
        JCMethodInvocation callToSuper = treeMaker.Apply(
                List.<JCExpression>nil(),
                pChSupp,
                List.<JCExpression>of(treeMaker.Ident(field.toName("listener"))));
        statements = List.<JCStatement>of(treeMaker.Exec(callToSuper));
        JCBlock methodBody = treeMaker.Block(0, statements);


        JCExpression methodVarType = chainDots(treeMaker, field, "java", "beans", "PropertyChangeListener");
        JCVariableDecl param =
                treeMaker.VarDef(
                treeMaker.Modifiers(Flags.FINAL, List.<JCAnnotation>nil()),
                field.toName("listener"),
                methodVarType, // java.beans.PropertyChangeListener
                null);


        List<JCVariableDecl> parameters = List.of(param);
        List<JCExpression> throwsClauses = List.nil(); //                 ok
        List<JCTypeParameter> methodGenericParams = List.nil(); //        ok
        JCExpression annotationMethodDefaultValue = null; //              ok
        Name methodName = field.toName("removePropertyChangeListener"); //    ok
        JCExpression methodType = treeMaker.Type(new Type(TypeTags.VOID, null)); //ok
        return treeMaker.MethodDef(
                treeMaker.Modifiers(access, List.<JCAnnotation>nil()),
                methodName,
                methodType,
                methodGenericParams,
                parameters,
                throwsClauses,
                methodBody,
                annotationMethodDefaultValue);
    }

    private boolean ensureAnnotIsOnClass(JavacNode annotationNode, JavacNode typeNode) {
        JCClassDecl typeDecl = null;
        if (typeNode.get() instanceof JCClassDecl) {
            typeDecl = (JCClassDecl) typeNode.get();
        }

        long flags = typeDecl == null ? 0 : typeDecl.mods.flags;
        boolean notAClass = (flags & (Flags.INTERFACE | Flags.ENUM | Flags.ANNOTATION)) != 0;

        if (typeDecl == null || notAClass) {
            annotationNode.addError("@AddPropertyChangeSupport is only supported on a class.");
            return false;
        }
        return true;
    }
}
